// 处理新连接相关的函数实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <errno.h>
#include <unistd.h>// close函数

#include "global.h"
#include "conf.h"
#include "net/netglobal.h"
#include "log.h"
#include "net/socket.h"

// 建立新连接的函数，当新连接进入时，本函数会被调用
void CSocket::Event_accept(lpconnection_t oldc){
    struct sockaddr mysockaddr;// 暂存客户端的socket地址结构体
    socklen_t socklen;// 暂存套接字长度
    int err;
    int s;// 暂存从内核获取的新连接套接字句柄
    static int use_accept4 = 1;// 先假设能够使用accept4()系统函数
    lpconnection_t newc;// 暂存连接池中的一个新连接

    socklen = sizeof(mysockaddr);
    
    // 使用do while是为了可以先尝试使用accept4()系统函数，并更改标记
    do{
        // listen套接字是非阻塞的，所以即便全连接队列为空也不会阻塞在这里
        if(use_accept4){
            // 使用accept4()系统函数可以直接设置标记，节省一次系统调用
            // 参数SOCK_NONBLOCK表示返回一个非阻塞的socket，节省一次ioctl【设置为非阻塞】系统调用
            s = accept4(oldc->fd, &mysockaddr, &socklen, SOCK_NONBLOCK);
        }
        else{
            s = accept(oldc->fd, &mysockaddr, &socklen);
        }

        if(s == -1){
            err = errno;// 记录错误码

            if(use_accept4 && err == ENOSYS){
                // 说明accept4()系统函数不可用，撤销标志重试
                use_accept4 = 0;
                continue;
            }

            if(err == EAGAIN){
                // 说明全连接队列是空的，直接返回
                return;
            }

            // 第一阶段进行日志记录
            if(err == ECONNABORTED){
                // 说明对方意外关闭套接字，如用户插拔网线、断电，比较常见
            }
            else if(err == EMFILE || err == ENFILE){
                // EMFILE:表示触发了进程级的资源限制，如fd已用尽
                // ENFILE:表示触发了系统全局的资源限制，如系统的fd已用尽
                Log_Info(err, "fd已用尽！");
            }

            // 第二阶段进行错误处理
            if(err == ECONNABORTED){
                // 这个错误可以忽略，不处理
            }
            else if(err == EMFILE || err == ENFILE){
                // 官方做法是先把读事件从listen socket上移除，然后再弄个定时器，定时器到了则继续执行该函数，但是定时器到了有个标记，会把读事件增加到listen socket上去；
                // 但太麻烦，先不处理
            }
            return;
        }

        // 走到这里，说明申请套接字句柄成功了

        // 检查是否超出最大设置的连接数
        if(m_onlineUserCount >= m_worker_connections){
            // 如果在线用户超过epoll的最大连接数了，就要关闭这个套接字
            close(s);// 因为还没分配连接，直接关闭就行
            return;
        }

        // 安全处理：恶意用户不断连接且发1条数据就断，会导致频繁调用get_connection()
        // 短时间内产生大量连接，危及本服务器安全
        // 不能超过5倍epoll最大数，由于延迟回收机制，这些短时间内产生的大量连接还没有被回收
        if(m_connectionList.size() > m_worker_connections * 5){
            // 整个连接池很大，但空闲连接却很少，说明确实被攻击了，不再接收新连接
            // 直到空闲连接变多了才恢复连接
            if(m_freeconnectionList.size() < m_worker_connections){
                close(s);
                return ;
            }
        }

        // 到这里说明服务器也正常，可以真正接受这个连接
        newc = Get_connection(s);// 获取连接池一个连接（没有也会创建新连接）
        memcpy(&newc->s_sockaddr, &mysockaddr, socklen);// //拷贝客户端地址到连接对象

        // 不是accept4()系统调用的话，就要设置标志为非阻塞
        if(!use_accept4){
            if(Setnonblocking(s) == false){
                Close_connection(newc);// 可以直接关闭这个连接和套接字（还没涉及到业务）
                return;// 设置失败直接返回
            }
        }

        // 连接对象和监听对象关联,方便通过连接对象找监听对象(关联到监听端口)
        newc->listening = oldc->listening;

        // 设置好读写函数
        newc->rhandler = &CSocket::Read_request_handler;
        newc->whandler = &CSocket::Write_request_handler;

        // 客户端应该主动发送一次数据，因此加入epoll的读事件监听
        if(Epoll_oper_event(s, EPOLL_CTL_ADD, EPOLLIN|EPOLLRDHUP, 0, newc) == -1){
            Close_connection(newc);// 加入失败可以直接关闭连接（还没涉及到业务）
            return;
        }

        // 可以增加测试打印缓冲区大小的代码，输出到日志上（采用默认的就行）

        // 判断是否开启心跳超时踢人
        if(m_ifkickTimeCount == 1){
            AddToTimerQueue(newc);// 加入超时检测队列
        }

        ++m_onlineUserCount;// 在线用户自增
        break;// 一般是循环一次就跳出了
    }while(true);
}